#include "sys/socket.h"
#include "netinet/in.h"
#include "arpa/inet.h"
#include <netdb.h>
#include <errno.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include "sys/types.h"
#include <pthread.h>
#include <sys/epoll.h>
#include <fcntl.h>

typedef void* wl_ext_tcp_handle;
typedef void* wl_ext_udp_handle;

enum {
    WL_EXT_NETWORK_SUCCESS = 0,
    WL_EXT_NETWORK_ERROR
};

/**
 * network callback
 * 
 * @param handle tcp handle
 * @param tcp_event network event
 * @param context user context
 */
typedef void (*wl_ext_tcp_callback)(wl_ext_tcp_handle handle, int tcp_event, void* context);

/**
 * get host by name
 * 
 * @param resultCode result code
 * @param ip ip
 * @param context user context
 *  
 */
typedef void (*wl_ext_get_host_callback)(int resultCode, const char* ip, void* context);

// network event
enum {
    WL_EXT_TCP_CONNECTING = 0, // tcp connecting
    WL_EXT_TCP_CONNECTED,      // tcp connected
    WL_EXT_TCP_DISCONNECT,     // tcp disconnected
    WL_EXT_TCP_CAN_SEND,       // sending available
    WL_EXT_TCP_CAN_RECV,       // receiving available
    WL_EXT_TCP_ERROR           // tcp error
};

typedef struct {
    char ip[32];
    int port;
    wl_ext_tcp_callback callback;
    void* context;
    int tcp_sock_fd;
    int local_sock_fd;
    pthread_t tcp_thread;
}wl_ext_tcp_handle_impl;


typedef struct {
    int udp_sock_fd;
    int port;
}wl_ext_udp_handle_impl;

typedef struct {
    int fd;
    char cmd[32];
}wl_ext_network_cmd;

/**
 * init network
 * 
 * @return 0 success, < 0 fail
 */
int wl_ext_network_init(void) {
    return 0;
}

/**
 * deinit network
 */
void wl_ext_network_deinit(void) {

}


static void wl_ext_network_send_cmd(wl_ext_tcp_handle handle, int fd, char *pCmd) {
    wl_ext_network_cmd cmd;
    wl_ext_tcp_handle_impl* handle_impl = (wl_ext_tcp_handle_impl*)handle;
    memset(&cmd, 0, sizeof(wl_ext_network_cmd));

    cmd.fd = fd;
    strcpy(cmd.cmd, pCmd);

    struct sockaddr_in send_addr;
    memset(&send_addr, 0, sizeof(send_addr));
    send_addr.sin_family = AF_INET;
    send_addr.sin_port = htons(8888);
    send_addr.sin_addr.s_addr = INADDR_ANY;

    sendto(handle_impl->local_sock_fd, &cmd, sizeof(wl_ext_network_cmd), 0, (struct sockaddr*)&send_addr, sizeof(send_addr));
}

static void* wl_ext_tcp_recv_task(void *pArg) {
    wl_ext_tcp_handle_impl *handle_impl = (wl_ext_tcp_handle_impl *)pArg;

    // create a udp socket for command communication
    int local_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (local_sock_fd < 0) {
        printf("create local socket fail\n");
        free(handle_impl);
        return NULL;
    }

    struct sockaddr_in local_addr;
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(8888);
    local_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(local_sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
        printf("bind local socket fail\n");
        close(local_sock_fd);
        free(handle_impl);
        return NULL;
    }

    handle_impl->local_sock_fd = local_sock_fd;

    int sock_fd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    if (sock_fd < 0) {
        printf("create socket fail\n");
        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
        goto task_end;
    }

    int flags = fcntl(sock_fd, F_GETFL, 0);

    if (flags == -1) {
        printf("get socket flags fail\n");
        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
        goto task_end;
    }

    if (fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK) == -1) {
        printf("set socket flags fail\n");
        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
        goto task_end;
    }

    struct sockaddr_in server_addr;
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(handle_impl->port);
    inet_pton(AF_INET, handle_impl->ip, &server_addr.sin_addr);

    int ret = connect(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr));
    if (ret < 0 && errno != EINPROGRESS) {
        printf("connect fail\n");
        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
        goto task_end;
    }

    int epoll_fd = epoll_create1(0);
    if (epoll_fd < 0) {
        printf("create epoll fail\n");
        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
        goto task_end;
    }

    if (ret < 0) {
        printf("connect in progress\n");
        struct epoll_event ev;
        ev.events = EPOLLOUT | EPOLLERR;
        ev.data.fd = sock_fd;
        epoll_ctl(epoll_fd, EPOLL_CTL_ADD, sock_fd, &ev);

        struct epoll_event events[10];
        int nfds = epoll_wait(epoll_fd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLOUT) {
                handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_CONNECTED, handle_impl->context);
                struct epoll_event ev;
                ev.events = EPOLLIN | EPOLLERR | EPOLLET;
                ev.data.fd = sock_fd;
                epoll_ctl(epoll_fd, EPOLL_CTL_DEL, sock_fd, NULL);
                epoll_ctl(epoll_fd, EPOLL_CTL_MOD, sock_fd, &ev);
                break;
            }

            if (events[i].events & EPOLLERR) {
                printf("connect error\n");
                handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
                goto task_end;             
            }
        }
    }

    handle_impl->tcp_sock_fd = sock_fd;

    struct epoll_event ev;
    ev.events = EPOLLIN | EPOLLERR | EPOLLET;
    ev.data.fd = local_sock_fd;
    epoll_ctl(epoll_fd, EPOLL_CTL_ADD, local_sock_fd, &ev);

    while (1) {
        struct epoll_event events[10];
        int nfds = epoll_wait(epoll_fd, events, 10, -1);
        for (int i = 0; i < nfds; ++i) {
            if (events[i].events & EPOLLIN) {
                if (events[i].data.fd == local_sock_fd) {
                    wl_ext_network_cmd cmd;
                    int len = recv(local_sock_fd, &cmd, sizeof(wl_ext_network_cmd), 0);
                    
                    if (len > 0) {
                        if (strncmp(cmd.cmd, "read", 4) == 0) {
                            ev.events = EPOLLIN | EPOLLERR | EPOLLET;
                            ev.data.fd = cmd.fd;
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, cmd.fd, NULL);
                            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, cmd.fd, &ev);
                        } else if (strncmp(cmd.cmd, "write", 5) == 0) {
                            ev.events = EPOLLOUT | EPOLLERR | EPOLLET;
                            ev.data.fd = cmd.fd;
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, cmd.fd, NULL);
                            epoll_ctl(epoll_fd, EPOLL_CTL_ADD, cmd.fd, &ev);
                        } else if (strncmp(cmd.cmd, "disconnect", strlen("disconnect")) == 0) {
                            handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_DISCONNECT, handle_impl->context);
                            epoll_ctl(epoll_fd, EPOLL_CTL_DEL, cmd.fd, NULL);
                            goto task_end;
                        }
                    }
                    break;
                }else {
                    if (handle_impl->callback) {
                        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_CAN_RECV, handle_impl->context);
                    }
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                }
            }

            if (events[i].events & EPOLLOUT) {
                if (events[i].data.fd == sock_fd) {
                    if (handle_impl->callback) {
                        handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_CAN_SEND, handle_impl->context);
                    }
                    epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, NULL);
                }
            }

            if (events[i].events & EPOLLERR) {
                printf("connect error\n");
                if (events[i].data.fd == sock_fd) {
                    handle_impl->callback((wl_ext_tcp_handle)handle_impl, WL_EXT_TCP_ERROR, handle_impl->context);
                    goto task_end;
                }
            }
        }
    }

    task_end:
        if (sock_fd > 0) {
            close(sock_fd);    
        }

        if (handle_impl->local_sock_fd > 0) {
            close(handle_impl->local_sock_fd);
        }
        
        if (epoll_fd > 0) {
            close(epoll_fd);
        }
        
        free(handle_impl);
        return NULL;
}

/**
 * connect to server
 * 
 * @param ip server ip
 * @param port server port
 * @param callback callback
 * @param context user context
 * 
 * @return tcp handle, non-null if success, null if fail
 */
wl_ext_tcp_handle wl_ext_tcp_connect(const char* ip, int port, wl_ext_tcp_callback callback, void* context) {
    wl_ext_tcp_handle_impl* handle_impl = (wl_ext_tcp_handle_impl*)malloc(sizeof(wl_ext_tcp_handle_impl));
    if (!handle_impl) {
        return NULL;
    }

    memset(handle_impl, 0, sizeof(wl_ext_tcp_handle_impl));
    strcpy(handle_impl->ip, ip);
    handle_impl->port = port;
    handle_impl->callback = callback;
    handle_impl->context = context;

    if (pthread_create(&handle_impl->tcp_thread, NULL, wl_ext_tcp_recv_task, handle_impl) != 0) {
        printf("create thread fail\n");
        free(handle_impl);
        return NULL;
    }
    pthread_detach(handle_impl->tcp_thread);

    return (wl_ext_tcp_handle)handle_impl;
}

/**
 * disconnect tcp
 * 
 * @param handle tcp handle
 */
void wl_ext_tcp_disconnect(wl_ext_tcp_handle handle) {
    wl_ext_tcp_handle_impl* handle_impl = (wl_ext_tcp_handle_impl*)handle;
    // send command "disconnect" to thread by local_sock_fd
    wl_ext_network_send_cmd(handle, handle_impl->tcp_sock_fd, "disconnect");

    // wait thread exit
    pthread_join(handle_impl->tcp_thread, NULL);
}

/**
 * send data, in blocked way
 * 
 * @param handle tcp handle
 * @param data data
 * @param len data length
 * 
 * @return > 0, means bytes actually read, < 0 means error, 0 means should waiting
 */
int wl_ext_tcp_send(wl_ext_tcp_handle handle, const char* data, int len) {
    wl_ext_tcp_handle_impl* handle_impl = (wl_ext_tcp_handle_impl*)handle;

    int ret = send(handle_impl->tcp_sock_fd, data, len, 0);
    if (ret < 0) {
        if (errno == EAGAIN) {
            // send write command to thread by local_sock_fd
            wl_ext_network_send_cmd(handle, handle_impl->tcp_sock_fd, "write");
            return 0; // should waiting
        } else {
            return -1; // error
        }
    }

    wl_ext_network_send_cmd(handle, handle_impl->tcp_sock_fd, "read");
    return ret; // bytes sent
}

/**
 * read data, in blocked way
 * 
 * @param handle tcp handle
 * @param buffer buffer
 * @param len buffer length
 * 
 * @return > 0, means bytes actually read, < 0 means error, 0 means should waiting
 */
int wl_ext_tcp_recv(wl_ext_tcp_handle handle, char* buffer, int len) {
    wl_ext_tcp_handle_impl* handle_impl = (wl_ext_tcp_handle_impl*)handle;

    //TODO: try to recv data from buffer
    int ret = recv(handle_impl->tcp_sock_fd, buffer, len, 0);
    if (ret < 0) {
        perror("recv error");
        if (errno == EAGAIN) {
            // send read command to thread by local_sock_fd
            wl_ext_network_send_cmd(handle, handle_impl->tcp_sock_fd, "read");
            return 0; // should waiting
        } else {
            return -1; // error
        }
    }

    wl_ext_network_send_cmd(handle, handle_impl->tcp_sock_fd, "read");
    return ret;
}


/**
 * create udp, and bind to local port
 * 
 * @param bindPort local port
 * 
 * @return udp handle, non-null if success, null if fail
 */
wl_ext_udp_handle wl_ext_udp_create(int bindPort) {
    wl_ext_udp_handle_impl* handle_impl = (wl_ext_udp_handle_impl*)malloc(sizeof(wl_ext_udp_handle_impl));
    if (handle_impl == NULL) {
        return NULL;
    }

    handle_impl->port = bindPort;
    handle_impl->udp_sock_fd = socket(AF_INET, SOCK_DGRAM, 0);
    if (handle_impl->udp_sock_fd < 0) {
        free(handle_impl);
        return NULL;
    }

    struct sockaddr_in local_addr;
    memset(&local_addr, 0, sizeof(local_addr));
    local_addr.sin_family = AF_INET;
    local_addr.sin_port = htons(bindPort);
    local_addr.sin_addr.s_addr = INADDR_ANY;

    if (bind(handle_impl->udp_sock_fd, (struct sockaddr*)&local_addr, sizeof(local_addr)) < 0) {
        close(handle_impl->udp_sock_fd);
        free(handle_impl);
        return NULL;
    }

    return (wl_ext_udp_handle)handle_impl;
}

/**
 * deinit udp
 *  
 * @param handle udp handle
 * @param ip server ip
 * @param port server port
 * @param data data
 * @param len data length
 * 
 * @return > 0, means bytes actually read, < 0 means error
 */
int wl_ext_udp_sendto(wl_ext_udp_handle handle, const char* ip, int port, const char* data, int len) {
    wl_ext_udp_handle_impl* handle_impl = (wl_ext_udp_handle_impl*)handle;

    struct sockaddr_in send_addr;
    memset(&send_addr, 0, sizeof(send_addr));
    send_addr.sin_family = AF_INET;
    send_addr.sin_port = htons(port);
    inet_pton(AF_INET, ip, &send_addr.sin_addr);

    return sendto(handle_impl->udp_sock_fd, data, len, 0, (struct sockaddr*)&send_addr, sizeof(send_addr));
}

/**
 * read data, in blocked way
 * 
 * @param handle udp handle
 * @param buffer buffer
 * @param len buffer length
 * 
 * @return > 0, means bytes actually read, < 0 means error
 */
int wl_ext_udp_recvFrom(wl_ext_udp_handle handle, char* buffer, int len) {
    wl_ext_udp_handle_impl* handle_impl = (wl_ext_udp_handle_impl*)handle;

    return recvfrom(handle_impl->udp_sock_fd, buffer, len, 0, NULL, NULL);
}


typedef struct {
    char name[128];
    wl_ext_get_host_callback callback;
    void* context;
}wl_ext_get_hostname_args;

static void* wl_ext_get_hostname_task(void* arg) {
    wl_ext_get_hostname_args* hostname_args = (wl_ext_get_hostname_args *)arg;
    struct hostent* host_info = gethostbyname(hostname_args->name);
    int resultCode = (host_info) ? 0 : -1;
    char* ip = (host_info && host_info->h_addr_list[0]) ? inet_ntoa(*(struct in_addr*)host_info->h_addr_list[0]) : NULL;
    if (hostname_args->callback) {
        hostname_args->callback(resultCode, ip, hostname_args->context);
    }
    free(hostname_args);
    return NULL;
}


/**
 * get host by name asynchronously
 * 
 * @param name host name
 * @param callback callback
 * @param context user context
 * 
 * @return 0 success, < 0 fail
 *  
 */
int wl_ext_get_host_by_name_async(const char* name, wl_ext_get_host_callback callback, void* context) {
    pthread_t thread;
    wl_ext_get_hostname_args *args = malloc(sizeof(wl_ext_get_hostname_args));
    
    if (!args) {
        return -1;
    }

    memset(args, 0, sizeof(wl_ext_get_hostname_args));
    strcpy(args->name, name);
    args->callback = callback;
    args->context = context;


    if (pthread_create(&thread, NULL, wl_ext_get_hostname_task, args) != 0) {
        free(args);
        return -1;
    }

    pthread_detach(thread);
    return 0;
}

/**
 * get host by name synchronously
 * 
 * @param name host name
 * @param ip ip
 * 
 * @return 0 success, < 0 fail
 *  
 */
int wl_ext_get_host_by_name_sync(const char* name, char* ip) {
    struct hostent* host_info = gethostbyname(name);
    int resultCode = (host_info) ? 0 : -1;
    strcpy(ip, (host_info && host_info->h_addr_list[0]) ? inet_ntoa(*(struct in_addr*)host_info->h_addr_list[0]) : "");
    return resultCode;
}


int wl_ext_is_network_valid(void) {
    return 1;
}